const got = require('got');
const { sleep } = require('./util');
const { debuglog } = require('util');
const debug = debuglog('consul');

class Consul {
  /**
   * consul操作类
   * @param {string} consulAddr ip:port
   * @param {string} token 
   */
  constructor(consulAddr, token) {
    this.consulAddr = consulAddr;
    this.token = token;
    this.api = got.extend({ prefixUrl: `http://${ consulAddr }/v1` });
  }

  /**
   * 获取目录下所有的kv
   * @param {string} dc 机房名称
   * @param {string} prefixKey key前缀，默认为 '/'
   * @returns {[Error, import('../typings/consul').ConsulKv[]]}
   */
  async getAllKv(dc, modifyIndex, prefixKey = '/') {
    try {
      /** @type { import('../typings/consul').ConsulKv[] } */
      const list = (await this.api.get(`kv${ prefixKey }`, {
        searchParams: {
          dc,
          recurse: '',
          index: modifyIndex,
        },
        headers: { 'X-Consul-Token': this.token },
        responseType: 'json',
      })).body;
      list.forEach((item) => {
        for (const [key, value] of Object.entries(item)) {
          // 大驼峰转小驼峰
          const newKey = key.replace(/^\w/, key.charAt(0).toLocaleLowerCase());
          if (newKey === key) {
            continue;
          }
          item[newKey] = value;
          Reflect.deleteProperty(item, key);
        }
        // 直接获取到的value是base64加密过的，此处解密方便后续使用
        if (item.value) {
          item.value = Buffer.from(item.value, 'base64').toString();
        }
      });
      return [null, list];
    } catch (error) {
      debug('getAllKv', dc, modifyIndex, prefixKey, error);
      return [error];
    }
  }

  /**
   * 获取所有的机房名称
   * @returns @returns {[Error, string[]]}
   */
  async getDcList() {
    try {
      const { body: list } = await this.api.get('catalog/datacenters', {
        responseType: 'json',
        headers: { 'X-Consul-Token': this.token },
      });
      return [null, list];
    } catch (error) {
      debug('getDcList', error);
      return [error];
    }
  }

  /**
   * 创建或更新一个key
   * @param {string} key key路径
   * @param {string} value 值
   * @param {string} dc 机房信息
   * @param {string} sessionId 是否加锁
   * @returns {[Error, boolean]}
   */
  async updateKey(key, value, dc, sessionId) {
    try {
      const query = { dc };
      if (sessionId) {
        query.acquire = sessionId;
      }
      const { body } = await this.api.put(`kv/${ key }`, {
        searchParams: query,
        body: value,
        headers: { 'X-Consul-Token': this.token },
      });
      return [null, body === 'true'];
    } catch (error) {
      debug('updateKey', key, value, dc, sessionId, error);
      return [error];
    }
  }

  /**
   * 获取key
   * @param {string} key 
   * @param {object} options
   * @param {string} options.dc 数据中心
   * @param {boolean} options.recurse 是否递归查询，默认false
   * @param {boolean} options.raw 只返回未编码的原始值，默认false
   * @param {boolean} options.keys 只返回子key列表，默认false
   * @param {string} options.separator  Specifies the string to use as a separator for recursive key lookups. This option is only used when paired with the keys parameter to limit the prefix of keys returned, only up to the given separator. This is specified as part of the URL as a query parameter.
   * @returns {[Error, import('../typings/consul').ConsulOriginKv[] | string | string[] ]}
   */
  async readKey(key, options = {}) {
    try {
      const info = (await this.api.get(`kv/${ key }`, {
        searchParams: options,
        responseType: options.raw ? 'text' : 'json',
        headers: { 'X-Consul-Token': this.token },
      })).body;
      return [null, info];
    } catch (error) {
      debug('readKey', key, options, error);
      return [error, []];
    }
  }

  /**
   * 创建session
   * @param {object} options 选项
   * @param {string} options.dc 数据中心
   * @param {string} options.LockDelay 解锁延迟时间，取值范围 [10s, 24h]
   * @param {string} options.Name session名称
   * @param {'release' | 'delete'} options.Behavior session失效后的策略，release表示与该会话相关的锁都会释放，delete表示持有该锁的key会被删除
   * @param {string} options.TTL 会话持续时间，指定后无法提前结束会话，取值范围 [10s, 86400s]
   * @param {string[]} options.Checks specifies a list of associated health check IDs (commonly CheckID in API responses). It is highly recommended that, if you override this list, you include the default serfHealth.
   * @param {string} options.Node  Specifies the name of the node. This must refer to a node that is already registered.
   * @returns {[Error, string]}
   */
  async createSession(options) {
    try {
      const { body } = await this.api.put('session/create', {
        json: options,
        headers: { 'X-Consul-Token': this.token },
        responseType: 'json',
      });
      return [null, body.ID];
    } catch (error) {
      debug(`createSession ${ JSON.stringify(options) }`, error);
      return [error];
    }
  }

  /**
   * 删除session
   * @param {string} sessionId 会话id
   * @param {string} dc 数据中心
   * @returns {[Error, boolean]}
   */
  async deleteSession(sessionId, dc) {
    try {
      const { body } = await this.api.put(`session/destroy/${ sessionId }`, {
        headers: { 'X-Consul-Token': this.token },
        searchParams: { dc },
      });
      return [null, body === 'true'];
    } catch (error) {
      debug(`deleteSession ${ sessionId } ${ dc }`, error);
      return [error];
    }
  }

  /**
   * 删除key
   * @param {string} key 目标key
   * @param {object} options 
   * @param {string} options.dc 数据中心
   * @param {boolean} options.recurse 是否精准匹配key，默认false
   * @param {number} options.cas 指定modifiIndex与consul中key的modifiIndex相同时才会被删除
   * @returns {[Error, boolean]}
   */
  async deleteKey(key, options) {
    try {
      const res = await this.api.delete(`kv/${ key }`, {
        headers: { 'X-Consul-Token': this.token },
        searchParams: options,
      });
      return [null, res.body === 'true'];
    } catch (error) {
      debug(`deleteKey ${ key }`, error);
      return [error];
    }
  }

  /**
   * 监听key
   * @param {string} dc 数据中心
   * @param {string} key 要监听的key
   * @param {() => void} callback 
   * @param {number} modifyIndex 
   */
  async watchKey(dc, key, callback, modifyIndex = 0) {
    while (true) {
      try {
        const info = await this.readKey(key, {
          dc,
          index: modifyIndex,
        });
        if (modifyIndex === info[0].ModifyIndex) {
          continue;
        }
        modifyIndex = info[0].ModifyIndex;
        const isBreak = callback(info);
        if (isBreak) {
          break;
        }
      } catch (error) {
        debug(`watch ${ dc } - ${ key }`, error);
        // 发生异常时，休眠1s，防止死循环吃空cpu
        await sleep(1000);
      }
    }
  }
}

exports.Consul = Consul;
